﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace BingSpatialDataServiceExcelAddin.BSDSTools.FileTools
{
    public class DelimitedReader : IDisposable
    {
        #region Private Properties

        private TextReader stream;
        private bool EOS = false;
        private bool EOL = false;
        private char[] buffer = new char[4096];
        private long pos = 0;
        private long length = 0;
        private char _delimiter;

        #endregion

        #region Constructor

        public DelimitedReader(string s, char delimiter)
        {
            _delimiter = delimiter;
            stream = new StringReader(s);
        }

        public DelimitedReader(TextReader s, char delimiter)
        {
            _delimiter = delimiter;
            stream = s;
        }

        public DelimitedReader(Stream s, char delimiter)
        {
            _delimiter = delimiter;
            stream = new StreamReader(s);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Disposes the CSVReader
        /// </summary>
        public void Dispose()
        {
            stream.Dispose();
            buffer = null;
        }

        /// <summary>
        /// Reads and breaks up the next row of the CSV file
        /// </summary>
        /// <returns>An array of string, each item in the array being a cell from the row</returns>
        public string[] GetNextRow()
        {
            List<string> row = new List<string>();

            //Loop through until every item in the row has been read
            while (true)
            {
                string item = GetNextItem();

                if (item == null)
                {
                    //Verify that the row contains data
                    return row.Count == 0 ? null : row.ToArray();
                }

                row.Add(item);
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Gets the next cell value from the CSV file
        /// </summary>
        /// <returns>Returns the value of a single cell in the CSV file</returns>
        private string GetNextItem()
        {
            if (EOL || EOS)
            {
                // previous item was last in line, start new line
                EOL = false;
                EOS = false;
                return null;
            }

            bool quoted = false;
            bool predata = true;
            bool postdata = false;
            StringBuilder item = new StringBuilder();

            while (true)
            {
                char c = GetNextChar(true);
                if (EOS)
                {
                    return item.Length > 0 ? item.ToString() : null;
                }

                if ((postdata || !quoted) && c == _delimiter)
                {
                    // end of item, return
                    return item.ToString();
                }

                if ((predata || postdata || !quoted) && (c == '\x0A' || c == '\x0D'))
                {
                    //We are at the end of the line, eat newline characters and exit
                    EOL = true;

                    if (c == '\x0D' && GetNextChar(false) == '\x0A')
                    {
                        // new line sequence is 0D0A
                        c = GetNextChar(true);
                    }

                    return item.ToString();
                }

                if (predata && c == ' ')
                {
                    // whitespace preceeding data, discard
                    continue;
                }

                if (predata && c == '"')
                {
                    // quoted data is starting
                    quoted = true;
                    predata = false;
                    continue;
                }

                if (predata)
                {
                    // data is starting without quotes
                    predata = false;
                    item.Append(c);
                    continue;
                }

                if (c == '"' && quoted)
                {
                    if (GetNextChar(false) == '"')
                    {
                        // double quotes within quoted string means add a quote       
                        item.Append(GetNextChar(true));
                    }
                    else
                    {
                        // end-quote reached
                        postdata = true;
                    }

                    continue;
                }

                // all cases covered, character must be data
                item.Append(c);
            }
        }

        /// <summary>
        /// Gets the next character from the stream
        /// </summary>
        /// <param name="eat"></param>
        /// <returns>The next character in the stream</returns>
        private char GetNextChar(bool eat)
        {
            if (pos >= length)
            {
                length = stream.ReadBlock(buffer, 0, buffer.Length);

                if (length == 0)
                {
                    EOS = true;
                    return '\0';
                }

                pos = 0;
            }

            if (eat)
            {
                return buffer[pos++];
            }
            else
            {
                return buffer[pos];
            }
        }

        #endregion
    }
}